在之前的好幾篇文章,談論主鍵 (prirmary key)、條件限制 (constraints)、索引 (index),我都主張 Datomic 是一種高階資料庫,提供了高階的語意,讓使用者不用想太多,因為很多『重要的決定』,Datomic 都幫你做了。
然而,『效能改進』恰好就是與高階對立: 高階語意是你用 API 就好,不用管底層的細節;效能改進卻往往是需要對底層的運作細節有點了解才能做的。
由於我本人並沒有在 Datomic 的公司工作,沒有機會一賭所有的 Datomic 底層運作細節,這邊只能基於我對 Datomic 底層運作的有限了解提出兩種在實際工作中用過的效能改進作法。
考慮如下的兩筆資料,其中,:person/roles
這個屬性它的 :db/cardinality
是 many
,所以它可以一對多。
[{:db/id "temp-1"
:person/name "John"
:person/roles [:driver :student]}
{:db/id "temp-2"
:person/name "Mary"
:person/roles [:driver :teacher]}]
如果我們已經有了第一筆資料的資料實體編碼 (entity id),想要查出,這個資料實體對應的 person/roles
是哪幾個時,可以怎麼做呢?
最直覺的作法是用查詢做。
(d/q '[:find ?r
:in $ ?e
:where [?e :person/roles ?r]]
(db/db) entity-id)
然而,它的效能普通,如果我們改成用『索引直接存取』,可以更快。
;;; Compare the speed of `d/datoms` and ordinary `d/q` query
(time (map :v
(d/datoms (db/db) :eavt
entity-id
attr-id)))
;; =>
;; (out) "Elapsed time: 0.091166 msecs"
;; (:driver :student)
(time (d/q '[:find ?r
:in $ ?e
:where [?e :person/roles ?r]]
(db/db) entity-id))
;; =>
;; (out) "Elapsed time: 2.878042 msecs"
;; #{[:student] [:driver]}
當然,上述的比較是簡單地用 Clojure 的 time
函數做一個極度簡單地比較。我多做了幾次實驗,如果是 d/q
這個函數的話,它的內部可能有很多最佳化的可能性,每次執行的結果還會不太一樣,從 2 msec 到 8 msec 都有。另一方面,datoms
的執行時間則穩定得多。
總結來講,d/datoms
是相對低階許多的存取方式,並不好寫,但是可以在某些關鍵時刻讓 Datomic 發揮極高的效能。
Datomic 的查詢非常地彈性而且有許多不同的用法,有人曾經實驗過,把 Datomic 當圖形資料庫來使用,發現查詢效能遠勝過 SQL 資料庫。
然而,它在 OLAP 的使用情境,查詢的效能還比 Postgres 差了 10 倍到 20 倍。(這個數字來自我的體感數字,沒有精確的量測。) 可惜,在 OLAP 的世界,Postgres 也都還不是最快的資料庫,要跟 DuckDB 比的話,都還遠遠比不上。
那…如果我們要改進的查詢效能,很不巧的就是 OLAP 的分析查詢,該怎麼辦?這種恰好是 Datomic 的弱項的事情,只好做變更擷取,直接把 Datomic 的內容,同步到 SQL 資料庫了。plenish 就是這樣子的一個函式庫,它可以幫你把 Datomic 的資料內容即時同步到 Postgres 。